/**
 * 输出依赖关系图的插件
 */

/**
 * @typedef {Object} ModuleInfoBase
 * @desc 每个Module对象的数据结构
 * @property {string} id - 名称
 * @property {boolean} isRoot - 名称
 * @property {{[string]: ModuleInfoBase}} dependency - 子依赖，指向另一个module的引用
 * @property {string[]} parents - 父级依赖id
 */

const fs = require("fs");
const { join } = require("path");
const path = require("path");

const pluginName = "DependenceTree";

// 判断是否是sfv拆分出来的文件
const isVueFile = (filename = "") => filename.includes(".vue?vue");

// 去掉 external 配置的文件
const isExternal = (filename = "") => filename.includes("external ");

// 不知道是什么，反正可以过滤掉
const isMulti = (filename = "") => filename.includes("multi ");
const removeMultiInPath = (filename = "") =>
  filename ? filename.replace("multi ", "") : filename;

// 还原被vue-loader处理过的sfc文件为原始路径
function formatVuePath(filename = "") {
  if (isVueFile(filename)) {
    const pathArrs = filename.split("!");
    const vuePath = pathArrs[pathArrs.length - 1];

    return vuePath.split("?")[0];
  }

  return filename;
}

// 其他loader搞过的路径
function formatLoaderPath(filename = "") {
  if (filename) {
    const pathArrs = filename.split("!");

    return pathArrs[pathArrs.length - 1];
  }

  return filename;
}

/**
 * 处理循环引用
 * @param {ModuleInfoBase} graphData
 */
function formatGraphToDGA(graphData, format = (val) => val) {
  const caches = new WeakMap();

  function dfs(graph) {
    if (caches.has(graph)) {
      const cached = caches.get(graph);

      // 循环引用
      if (cached.dependency === void 0) return "circular reference";

      return cached.dependency;
    }

    const resultObj = {};
    caches.set(graph, resultObj);

    const dependency = graph.dependency;
    const children = {};
    for (let key in dependency) {
      if (Object.prototype.hasOwnProperty.call(dependency, key)) {
        children[key] = dfs(dependency[key]);
      }
    }

    const formData = format({ ...graph, dependency: children });
    Object.assign(resultObj, formData);

    return resultObj;
  }

  return dfs(graphData);
}

class DependenceTree {
  constructor(options = {}) {
    this._options = {
      filename: pluginName.toLocaleLowerCase(), // 输出的文件名

      // 排除的文件
      excludes: ["node_modules", /\.(png|jpg|svg|gif|eot|ttf|woff|woff2|ico)$/],
      format: ({ dependency }) => ({ dependency }), // 格式化输出的对象，默认只输出依赖的数据 dependency
      ...options,
    };

    this.moduleInfoGraph = new Map();
  }

  apply(complier) {
    const { filename, format } = this._options;
    complier.hooks.done.tap(pluginName, (stats, callback) => {
      // 获取stats对象的数据，去掉 source(源文件内容) 的数据
      const statsObj = stats.toJson({ source: false });
      const entryPoints = statsObj.entryPoints || {};
      const chunks = statsObj.chunks || [];

      // 生成关系树
      Object.keys(entryPoints).forEach((chunkName) => {
        const chunkID = entryPoints[chunkName].chunks[0];
        const chunkObj = chunks.find((item) => item.id === chunkID);

        chunkObj && this.createTree(chunkObj, chunks);
      });

      // 输出
      this.outputJSON(path.join(process.cwd(), `${filename}.json`), format);
      callback && callback();
    });
  }

  createTree(curChunk, chunks) {
    const { modules: chunkModules = [], children: chunkChildren = [] } =
      curChunk;

    // 遍历这个chunk打包进的所有modules
    // reasons: 模块被引入的信息，包含其父模块的路径，代码的位置，即谁依赖了它
    chunkModules.forEach(({ name = "", reasons = [] }) => {
      if (!name || this.isIgnoreFile(filename)) {
        return;
      }

      const moduleInfo = this.getModuleInfo(this.formatName(name));

      reasons.forEach((reasonItem) => {
        if (
          !reasonItem.module ||
          (isMulti(reasonItem.module) && reasonItem.type !== "single entry")
        ) {
          return;
        }

        const parentName = this.formatName(reasonItem.module);
        const parentInfo = this.formatName(parentName);

        // 表示这个一个入口，此时的文件等于当前module的路径
        if (reasonItem.type === "single entry") {
          parentInfo.isRoot = true;
          return;
        }

        // 添加进父的依赖中
        if (!parentInfo.dependency[moduleInfo.id]) {
          parentInfo.dependency[moduleInfo.id] = moduleInfo;
          moduleInfo.parents.push(parentInfo.id);
        }
      });
    });

    // 处理子chunk
    chunkChildren.forEach((chunkID) => {
      const chunkObj = chunks.find((item) => item.id === chunkID);

      chunkObj && this.createTree(chunkObj, chunks);
    });
  }

  formatName(name = "") {
    return [formatLoaderPath, removeMultiInPath, formatVuePath].reduce(
      (prev, curFn) => curFn(prev),
      name
    );
  }

  getModuleInfo(moduleName = "") {
    this.createModuleInfo(moduleName);

    return this.moduleInfoGraph.get(moduleName);
  }

  createModuleInfo(moduleName = "") {
    let info = this.moduleInfoGraph.get(moduleName);

    if (moduleName && !info) {
      this.moduleInfoGraph.set(moduleName, {
        isRoot: false,
        id: moduleName,
        dependency: {},
        parents: [],
      });
    }
  }

  outputJSON(filename, format) {
    const data = {};
    for (let info of this.moduleInfoGraph.values()) {
      if (info.isRoot) {
        data[info.id] = formatGraphToDGA(info, format);
      }
    }

    fs.writeFileSync(filename, JSON.stringify(data, null, 4), { flag: "w+" });
  }

  isIgnoreFile(fielname) {
    const { excludes } = this._options;

    if ([isVueFile, isExternal, isMulti].some((valid) => valid(fielname))) {
      return true;
    }

    return excludes.some((item) => {
      if (typeof item === "funtion") {
        return item(fielname);
      } else if (typeof item === "string") {
        return item.includes(filename);
      } else if (Object.prototype.toString.call(item) === "[object RegExp]") {
        return item.test(filename);
      }

      return false;
    });
  }
}

module.exports = DependenceTree;
